/*-------------------------------------------------------------------------
 *
 * condition_variable.c
 *	  条件变量的实现。条件变量提供了一种让一个进程等待特定条件发生的方法，
 *	  不需要知道正在等待的进程的具体身份。条件变量的等待可以被中断，
 *	  与 LWLock 等待不同。条件变量在动态共享内存段中使用是安全的。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/storage/lmgr/condition_variable.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "miscadmin.h"
#include "portability/instr_time.h"
#include "storage/condition_variable.h"
#include "storage/ipc.h"
#include "storage/proc.h"
#include "storage/proclist.h"
#include "storage/spin.h"
#include "utils/memutils.h"

/* 最初，我们不准备在任何条件变量上休眠。 */
static ConditionVariable *cv_sleep_target = NULL;

/*
 * 初始化条件变量。
 */
void ConditionVariableInit(ConditionVariable *fc_cv)
{
	SpinLockInit(&fc_cv->mutex);
	proclist_init(&fc_cv->wakeup);
}

/*
 * 准备在给定的条件变量上等待。
 *
 * 这可以在进入测试/睡眠循环之前可选地调用。
 * 如果我们至少需要睡眠一次，这样做是更有效的。
 * 但是，如果退出条件的第一次测试可能成功，
 * 则省略 ConditionVariablePrepareToSleep 调用会更有效。
 * 有关更多细节，请参见 ConditionVariableSleep 中的注释。
 *
 * 注意：“在进入循环之前”意味着您必须在调用 ConditionVariablePrepareToSleep 和调用
 * ConditionVariableSleep 之间测试退出条件。如果这样做不方便，可以省略调用
 * ConditionVariablePrepareToSleep。
 */
void ConditionVariablePrepareToSleep(ConditionVariable *fc_cv)
{
	int			fc_pgprocno = MyProc->pgprocno;

	/*
	 * 如果其他睡眠已经准备好，则取消它；这是必要的
	 * 因为我们只有一个静态变量跟踪准备的睡眠，
	 * 而且在我们的 PGPROC 中只有一个 cvWaitLink。这样做是可以的
	 * 因为每当控制返回到另一个测试和睡眠循环时，
	 * 它的 ConditionVariableSleep 调用将重新建立该睡眠作为
	 * 准备好的睡眠。
	 */
	if (cv_sleep_target != NULL)
		ConditionVariableCancelSleep();

	/* 记录我们将要睡眠的条件变量。 */
	cv_sleep_target = fc_cv;

	/* 将自己添加到等待队列中。 */
	SpinLockAcquire(&fc_cv->mutex);
	proclist_push_tail(&fc_cv->wakeup, fc_pgprocno, cvWaitLink);
	SpinLockRelease(&fc_cv->mutex);
}

/*
 * 等待给定的条件变量被信号通知。
 *
 * 这应该在一个测试特定退出条件并在其他情况下睡眠的谓词循环中调用，如下所示：
 *
 *	 ConditionVariablePrepareToSleep(cv);  // 可选
 *	 while (我们等待的条件不为真)
 *		 ConditionVariableSleep(cv, wait_event_info);
 *	 ConditionVariableCancelSleep();
 *
 * wait_event_info 应该是从 pgstat.h 中定义的 WaitEventXXX 枚举之一的值。
 * 这控制在等待时 pg_stat_activity 的 wait_event_type 和 wait_event 列的内容。
 */
void ConditionVariableSleep(ConditionVariable *fc_cv, uint32 fc_wait_event_info)
{
	(void) ConditionVariableTimedSleep(fc_cv, -1 /* 无超时 */ ,
									   fc_wait_event_info);
}

/*
 * 等待条件变量被信号通知或超时到达。
 *
 * 超时到期时返回 true，否则返回 false。
 *
 * 有关一般用法，请参见 ConditionVariableSleep()。
 */
bool ConditionVariableTimedSleep(ConditionVariable *fc_cv, long fc_timeout,
							uint32 fc_wait_event_info)
{
	long		fc_cur_timeout = -1;
	instr_time	fc_start_time;
	instr_time	fc_cur_time;
	int			fc_wait_events;

	/*
	 * 如果调用者没有显式准备睡眠，那么现在就准备并立即返回。
	 * 如果调用者的谓词循环尚未满足退出条件，则应立即再次调用。
	 * 这将导致退出条件在我们第一次睡眠之前被测试两次。
	 * 通过首先调用 ConditionVariablePrepareToSleep(cv) 可以避免额外的测试。
	 * 是否值得这样做取决于您是否期望退出条件最初满足，
	 * 在这种情况下，跳过准备是推荐的，因为它避免了对等待列表的操作，
	 * 或者最初未满足，在这种情况下，首先准备更好，因为它
	 * 避免了对退出条件的额外测试。
	 *
	 * 如果我们当前准备在其他条件变量上睡眠，我们只需取消
	 * 那个并准备这个；参见 ConditionVariablePrepareToSleep。
	 */
	if (cv_sleep_target != fc_cv)
	{
		ConditionVariablePrepareToSleep(fc_cv);
		return false;
	}

	/*
	 * 记录当前时间，以便在我们被虚假唤醒时能计算剩余超时。
	 */
	if (fc_timeout >= 0)
	{
		INSTR_TIME_SET_CURRENT(fc_start_time);
		Assert(fc_timeout >= 0 && fc_timeout <= INT_MAX);
		fc_cur_timeout = fc_timeout;
		fc_wait_events = WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH;
	}
	else
		fc_wait_events = WL_LATCH_SET | WL_EXIT_ON_PM_DEATH;

	while (true)
	{
		bool		fc_done = false;

		/*
		 * 等待 latch 被设置。（如果我们因其他原因被唤醒，下面的代码也能应对。）
		 */
		(void) WaitLatch(MyLatch, fc_wait_events, fc_cur_timeout, fc_wait_event_info);

		/* 在检查等待列表的状态之前重置 latch。 */
		ResetLatch(MyLatch);

		/*
		 * 如果这个进程已被移出等待列表，那么我们知道
		 * 它已被 ConditionVariableSignal（或
		 * ConditionVariableBroadcast）信号通知，因此我们应该返回给调用者。
		 * 但这并不保证退出条件已满足，仅仅是我们应该检查它。
		 * 因此，我们必须将进程重新放回等待列表中，以确保在
		 * 调用者检查其退出条件时不会错过任何额外的唤醒。
		 * 只有当调用者调用
		 * ConditionVariableCancelSleep 时，我们才能将自己移出
		 * 等待列表。
		 *
		 * 如果我们仍在等待列表中，则 latch 一定是被
		 * ConditionVariableSignal 以外的其他东西设置的；
		 * 尽管我们不保证不会虚假返回，但我们会避免这种明显的情况。
		 */
		SpinLockAcquire(&fc_cv->mutex);
		if (!proclist_contains(&fc_cv->wakeup, MyProc->pgprocno, cvWaitLink))
		{
			fc_done = true;
			proclist_push_tail(&fc_cv->wakeup, MyProc->pgprocno, cvWaitLink);
		}
		SpinLockRelease(&fc_cv->mutex);

		/*
		 * 检查中断，如果导致当前睡眠目标发生变化，
		 * 则虚假返回（这意味着中断处理程序代码
		 * 等待了一个不同的条件变量）。
		 */
		CHECK_FOR_INTERRUPTS();
		if (fc_cv != cv_sleep_target)
			fc_done = true;

		/* 我们被信号通知了，因此返回 */
		if (fc_done)
			return false;

		/* 如果我们还没有完成，更新下次迭代的 cur_timeout */
		if (fc_timeout >= 0)
		{
			INSTR_TIME_SET_CURRENT(fc_cur_time);
			INSTR_TIME_SUBTRACT(fc_cur_time, fc_start_time);
			fc_cur_timeout = fc_timeout - (long) INSTR_TIME_GET_MILLISEC(fc_cur_time);

			/* 我们是否超出了超时阈值？ */
			if (fc_cur_timeout <= 0)
				return true;
		}
	}
}

/*
 * 取消任何待处理的睡眠操作。
 *
 * 我们只需要将自己从任何我们之前准备过的条件变量的等待队列中移除。
 *
 * 如果没有任何待处理的操作，则不进行任何操作；
 * 这允许在事务中止时调用此函数以清理任何未完成的 CV 睡眠。
 */
void ConditionVariableCancelSleep(void)
{
	ConditionVariable *fc_cv = cv_sleep_target;
	bool		fc_signaled = false;

	if (fc_cv == NULL)
		return;

	SpinLockAcquire(&fc_cv->mutex);
	if (proclist_contains(&fc_cv->wakeup, MyProc->pgprocno, cvWaitLink))
		proclist_delete(&fc_cv->wakeup, MyProc->pgprocno, cvWaitLink);
	else
		fc_signaled = true;
	SpinLockRelease(&fc_cv->mutex);

	/*
	 * 如果我们已收到信号，则将其传递给另一个等待的进程（如果有的话）。
	 * 否则，调用 ConditionVariableSignal() 可能会丢失，
	 * 尽管还有另一个进程准备处理它。
	 */
	if (fc_signaled)
		ConditionVariableSignal(fc_cv);

	cv_sleep_target = NULL;
}


/*
 * 唤醒在条件变量上睡眠的最老进程（如果有的话）。
 *
 * 注意：很难判断这是否真的有效：我们知道
 * 是否从列表中移除了一个条目，但该条目可能只是一
 * 个哨兵。因此，在提议这应该返回一个标志来指示是否
 * 唤醒了某人之前，请三思而行。
 */
void ConditionVariableSignal(ConditionVariable *fc_cv)
{
	PGPROC	   *fc_proc = NULL;

	/* 从唤醒队列中移除第一个进程（如果有的话）。 */
	SpinLockAcquire(&fc_cv->mutex);
	if (!proclist_is_empty(&fc_cv->wakeup))
		fc_proc = proclist_pop_head_node(&fc_cv->wakeup, cvWaitLink);
	SpinLockRelease(&fc_cv->mutex);

	/* 如果我们发现有人在睡觉，则设置他们的闩以唤醒他们。 */
	if (fc_proc != NULL)
		SetLatch(&fc_proc->procLatch);
}

/*
 * 唤醒所有在给定条件变量上睡眠的进程。
 *
 * 这保证会唤醒在调用时正在条件变量上睡眠的所有进程，
 * 但是在调用过程中自己添加到列表中的进程通常不会被
 * 唤醒。
 */
void ConditionVariableBroadcast(ConditionVariable *fc_cv)
{
	int			fc_pgprocno = MyProc->pgprocno;
	PGPROC	   *fc_proc = NULL;
	bool		fc_have_sentinel = false;

	/*
	 * 在某些用例中，唤醒的进程通常会立即重新排队。
	 * 如果我们只是天真地尝试将唤醒列表减少为空，那么
	 * 我们将陷入一个针对这样的进程的潜在无限循环。
	 * 我们真的想要的语义只是确保我们已经唤醒了
	 * 进入时在列表中的所有进程。我们可以使用我们自己的
	 * cvWaitLink 作为哨兵，以检测我们何时完成。
	 *
	 * 这种方法的一个显而易见的缺陷是，其他人可能会发出
	 * 信号到条件变量，并在这样做时移除我们的哨兵条目。
	 * 但这没关系：由于条件变量的等待者总是按照顺序
	 * 添加和移除，这必然意味着每个先前的等待者都已被唤醒，
	 * 因此我们完成了。我们将会从其他人的信号中获得一个
	 * 额外的“设置”在我们的闩上，这稍微低效但无害。
	 *
	 * 如果我们的 cvWaitLink 已经在某个其他的风格列表中使用，
	 * 我们就无法将其插入作为哨兵。虽然对该函数的典型用法
	 * 不应是这样的，但我们可以通过简单地取消任何已准备的
	 * 条件变量睡眠来处理它。下一次调用 ConditionVariableSleep
	 * 将负责重新建立丢失的状态。
	 */
	if (cv_sleep_target != NULL)
		ConditionVariableCancelSleep();

	/*
	 * 检查队列的状态。如果为空，我们就没什么好做的。
	 * 如果正好有一个条目，我们只需移除并发出信号。
	 * 否则，移除第一个条目并插入我们的哨兵。
	 */
	SpinLockAcquire(&fc_cv->mutex);
	/* 在这里的时候，让我们断言我们不在列表中。 */
	Assert(!proclist_contains(&fc_cv->wakeup, fc_pgprocno, cvWaitLink));

	if (!proclist_is_empty(&fc_cv->wakeup))
	{
		fc_proc = proclist_pop_head_node(&fc_cv->wakeup, cvWaitLink);
		if (!proclist_is_empty(&fc_cv->wakeup))
		{
			proclist_push_tail(&fc_cv->wakeup, fc_pgprocno, cvWaitLink);
			fc_have_sentinel = true;
		}
	}
	SpinLockRelease(&fc_cv->mutex);

	/* 唤醒第一个等待者（如果有的话）。 */
	if (fc_proc != NULL)
		SetLatch(&fc_proc->procLatch);

	while (fc_have_sentinel)
	{
		/*
		 * 每次循环时，移除唤醒列表的第一个条目，并
		 * 发送信号，除非它是我们的哨兵。只要哨兵
		 * 保持在列表中，就重复这个过程。
		 *
		 * 请注意，如果其他人移除了我们的哨兵，我们将在退出
		 * 之前唤醒一个额外的进程。这是故意的，因为如果
		 * 其他人发出信号到条件变量，他们可能打算唤醒某个
		 * 第三个在我们添加哨兵后自己添加到列表中的进程。
		 * 给一个虚假的唤醒（这应该只是在浪费一些周期上
		 * 无害）总比失去一个唤醒要好。
		 */
		fc_proc = NULL;
		SpinLockAcquire(&fc_cv->mutex);
		if (!proclist_is_empty(&fc_cv->wakeup))
			fc_proc = proclist_pop_head_node(&fc_cv->wakeup, cvWaitLink);
		fc_have_sentinel = proclist_contains(&fc_cv->wakeup, fc_pgprocno, cvWaitLink);
		SpinLockRelease(&fc_cv->mutex);

		if (fc_proc != NULL && fc_proc != MyProc)
			SetLatch(&fc_proc->procLatch);
	}
}
